/**
 * 
 */
package net.bingosoft.common.json;

import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 用于构造JSON格式文本的简单工具类
 * @author elvis
 */
public final class JSONEncoder {
    
    public static final String NULL_STRING = "null";
    
    public static final String EMPTY_OBJECT = "{}";
    
    /** 是否对所有字符串都进行trim */
    private boolean trimSpace  = true;
    
    /** 是否忽略值为null的属性*/
    private boolean ignoreNull = true;
    
    /** 是否忽略值为空白符的属性 */
    private boolean ignoreEmpty = true; 
    
    private Map<String, String> aliases = new HashMap<String, String>();
    
    private Map<String,Object> ignores  = new HashMap<String,Object>();
    
    private Map<String,Object> includes = new HashMap<String, Object>();
    
    public JSONEncoder(){
        
    }
    
    public void alias(String alias,String property){
        this.aliases.put("prop$" + property, alias);
    }
    
    public void alias(String alias,Class<?> type,String property){
        this.aliases.put(type.getName() + "$" + property, alias);
    }
    
    public void alias(String alias,Class<?> type){
        this.aliases.put("type$" + type.getName(), alias);
    }
    
    public void ignore(String property){
        this.ignores.put("prop$" + property,null);
    }
    
    public void ignore(Class<?> type,String property){
        this.ignores.put(type.getName() + "$" + property,null);
    }
    
    public void ignore(Class<?> type){
        this.ignores.put("type$" + type.getName(),null);
    }
    
    public void ignore(String property,Object value){
        this.ignores.put("prop$" + property,value);
    }
    
    public void ignore(Class<?> type,Object value){
        this.ignores.put("type$" + type.getName(),value);
    }  
    
    public void ignore(Class<?> parent,Class<?> type){
        this.ignores.put("parent$" + parent.getName() + ",type$" + type.getName(),null);
    }
    
    public void ignore(Class<?> type,String property,Object value){
        this.ignores.put(type.getName() + "$" + property,value);
    } 
    
    public void include(String property){
        this.includes.put("prop$" + property,null);
    }
    
    public void include(Class<?> type,String property){
        this.includes.put(type.getName() + "$" + property,null);
    }
    
    public void include(Class<?> type){
        this.includes.put("type$" + type.getName(),null);
    }
    
    public void include(String property,Object value){
        this.includes.put("prop$" + property,value);
    }
    
    public void include(Class<?> type,Object value){
        this.includes.put("type$" + type.getName(),value);
    }   
    
    public void include(Class<?> parent,Class<?> type){
        this.includes.put("parent$" + parent.getName() + ",type$" + type.getName(),null);
    }    
    
    public void include(Class<?> type,String property,Object value){
        this.includes.put(type.getName() + "$" + property,value);
    }     
    
    public boolean isTrimSpace() {
        return trimSpace;
    }

    public void setTrimSpace(boolean trimSpace) {
        this.trimSpace = trimSpace;
    }
    
    public boolean isIgnoreNull() {
        return ignoreNull;
    }

    public void setIgnoreNull(boolean ignoreNull) {
        this.ignoreNull = ignoreNull;
    }
    
    public boolean isIgnoreEmpty() {
        return ignoreEmpty;
    }

    public void setIgnoreEmpty(boolean ignoreEmpty) {
        this.ignoreEmpty = ignoreEmpty;
    }

    protected String getAlias(Class<?> objectType,Class<?> propType,String prop){
        String alias = aliases.get(objectType.getName() + "$" + prop);
        if(null == alias){
            alias = aliases.get("prop$" + prop);
            if(null == alias){
                alias = aliases.get("type$" + propType.getName());
            }
        }
        return alias;
    }
    
    protected boolean isInlcude(Class<?> objectType,Class<?> propType,String propName,Object propValue){
        return isMatch(includes, objectType, propType, propName, propValue);
    }
    
    protected boolean isIgnore(Class<?> objectType,Class<?> propType,String propName,Object propValue){
        boolean include = isInlcude(objectType, propType, propName, propValue);
        if(include){
            return false;
        }else{
            return isMatch(ignores, objectType, propType, propName, propValue);    
        }
    }
    
    protected boolean isMatch(Map<String, Object> map, Class<?> objectType,Class<?> propType,String propName,Object propValue){
        String matchKey = objectType.getName() + "$" + propName;
        boolean isMatch = map.containsKey(matchKey);
        if(!isMatch){
            matchKey = "prop$" + propName;
            isMatch  = map.containsKey(matchKey);
            if(!isMatch){
                matchKey = "parent$" + objectType.getName() + ",type$" + propType.getName();
                isMatch  = map.containsKey(matchKey);
                if(!isMatch){
                    matchKey = "type$" + propType.getName();
                    isMatch  = map.containsKey(matchKey);
                }
            }
        }
        
        if(isMatch){
            Object value = map.get(matchKey);
            if(null != value && !value.equals(propValue)){
                isMatch = false;
            }
        }
        
        return isMatch;  
    }
    
    protected String getPropName(Class<?> objectType,Class<?> propType,String prop){
        String alias = getAlias(objectType, propType, prop);
        return alias == null ? prop : alias;
    }
    
    @SuppressWarnings("unchecked")
    public String encode(Object object){
        if(null != object){
            Class<?> objectType = object.getClass();
            if(object instanceof Collection){
                return encodeColletion((Collection)object);
            }else if(object instanceof Map){
                return encodeMap((Map)object);
            }else if(object.getClass().isArray()){
                return encodeArray(object);
            }else if(isNumberType(objectType)){
                return encodeNumber(object);
            }else if(isBooleanType(objectType)){
                return encodeBoolean(object);
            }else if(isByteType(objectType)){
                return encodeByte(object);
            }else if(isStringType(objectType)){
                return encodeString(object);
            }else{
                JSONBuilder json = new JSONBuilder(trimSpace,ignoreNull);
                json.startObject();
                try {
                    Class<?> clazz = object.getClass();
                    PropertyDescriptor[] propDescs = Introspector.getBeanInfo(clazz, Introspector.USE_ALL_BEANINFO).getPropertyDescriptors();
                    for (PropertyDescriptor propDesc : propDescs){
                        Method getter = propDesc.getReadMethod();
                        
                        if(null != getter){
                            String propName = propDesc.getName();
                            if(!propName.equalsIgnoreCase("class")){
                                Class<?> type    = propDesc.getPropertyType();
                                Object propValue = getter.invoke(object);
                                
                                if(!(ignoreNull && null == propValue) && !isIgnore(object.getClass(), type, propName, propValue)){
                                    String key   = JSONUtil.quote(getPropName(clazz, type, propName));
                                    String value = encode(propValue);
                                    if(!isIgnoreEmpty() || !(value.equals("''") || value.equals("\"\""))){
                                        json.setPropertyQuoted(key, value);
                                    }
                                }
                            }
                        }
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e.getMessage(),e);
                }
                json.endObject();
                return json.toString();
            }   
        }else{
            return EMPTY_OBJECT;
        }
    }
    
    protected String encodeArray(Object array){
        JSONBuilder json = new JSONBuilder(trimSpace,ignoreNull);
        json.startArray();        
        int len = Array.getLength(array);
        for(int i=0;i<len;i++){
            Object o = Array.get(array, i);
            if(null != o){
                json.setValueQuoted(encode(o));
            }
        }
        json.endArray();
        return json.toString();
    }
    
    protected String encodeColletion(Collection<Object> col){
        JSONBuilder json = new JSONBuilder(trimSpace,ignoreNull);
        json.startArray();
        
        for(Object o : col){
            if(null != o){
                json.setValueQuoted(encode(o));    
            }
        }
        json.endArray();
        return json.toString();        
    }
    
    protected String encodeMap(Map<Object,Object> map){
        Set<Object> keySet = map.keySet();
        
        JSONBuilder json = new JSONBuilder(trimSpace,ignoreNull);
        json.startObject();      
        for(Object o : keySet){
            Object key = o.toString();
            Object value = map.get(o);
            if(!(isIgnoreNull() && null == value)){
                String prop = getPropName(map.getClass(), key.getClass(), key.toString());
                json.setPropertyQuoted(JSONUtil.quote(prop), encode(value));    
            }
        }     
        json.endObject();
        return json.toString();
    }
    
    protected String encodeString(Object value){
        return null == value ? "''" : JSONUtil.quote((trimSpace ? value.toString().trim() : value.toString()));
    }
    
    protected String encodeNumber(Object value){
        return null == value ? "0" : String.valueOf(value);
    }
    
    protected String encodeBoolean(Object value){
        return null == value ? "false" : String.valueOf(value).toLowerCase();
    }    
    
    protected String encodeByte(Object value){
        return "0x" + (null == value ? "0" : JSONUtil.toHexString(((Byte)value).byteValue()));
    }
    
    static boolean isNumberType(Class<?> type){
        return type == Integer.TYPE || type == Integer.class || 
                    type == Short.TYPE || type == Short.class ||
                    type == Long.TYPE || type == Long.class ||
                    type == Float.TYPE || type == Float.class ||
                    type == Double.TYPE || type == Double.class;
                    
    }
    
    static boolean isBooleanType(Class<?> type){
        return type == Boolean.TYPE || type == Boolean.class;
    }
    
    static boolean isByteType(Class<?> type){
        return type == Byte.TYPE || type == Byte.class;
    }
    
    static boolean isStringType(Class<?> type){
        return type == String.class || type == Character.TYPE || type == Character.class;
    }
}
